E2EEを実現するための暗号技術 | Anonify解体新書3
前回の内容はこちら
TL;DR
セキュリティ・プライバシー保護技術Anonifyの主要技術要素について解説する連載記事(全8回)
Anonifyで使用されている技術要素を洗い出し重要な技術について簡単なサンプルプログラムを交えて解説する。
その3では「E2EEを実現するための暗号技術」と題して、Enclaveの中でcrypto_boxを使った鍵交換プログラムを書く方法について解説する。
サンプルプログラムはRustで開発
Anonifyではデータの秘匿性を担保するため、E2EEを使ってクライアントプログラムと通信をする。
E2EEの実装にはcrypto_boxとデータのシーリングを組み合わせる必要があり、Anonifyでもそのような実装になっている。
crypto_boxはデータの暗号化技術、データのシーリングは鍵の安全な管理
データのシーリングについては次回取り上げる。
ここで使用するコードはすべて独立して動作するのでAnonify自体の知識やAnonifyの動作環境は不要
この記事の中で使用する図は特別な記載がない限り全て筆者が作成したもの
サンプルプログラムはこちら
Anonifyが使用している主な技術要素
TEE(Intel SGX)関連
OCall/ECall
Remote Attestation
crypto_box(NaCl) <- 今回解説するのはここ
データのシーリング
mutual-TLS
Blockchain関連
スマートコントラクト
Web3
HTTPSで通信すればE2EEは不要なのでは
E2EEは暗号化を使用する利用者のみが鍵を持つことで第三者が勝手にデータを復号することを防ぐ技術
HTTPSだと通信経路の暗号化しかできない
E2EEはメッセンジャーサービスや電子メールでの利用例が多い
メッセージをやりとりするクライアント同士が鍵を交換してメッセージを暗号化する
メッセージを復号できるのはメッセージをやりとりしている当事者のみ
cipepser.icon そぼぎ:当事者同士でTLSのコネクション結んだらいいのでは?
glassonion1.icon 完全P2PだとTLSで良さそう
jkcomment.icon 余談ですが、WebRTCが完全P2Pで、TLS(DTLS)を使ってますが、E2EE必要だよねと動きもあったりしますね(ブラウザでの実現は難しかった(特にWebRTCでは不可能)が、最近ブラウザ側がInsertable Streams API を実装したため、WebRTC でもE2EE を実現できるようになったらしいです)
glassonion1.icon WebRTC詳しくないのであれなんですが正確にはこちらのツイートを参照するのが良さそうです
メッセンジャーサービスの場合、サーバ側には暗号メッセージしか保存されておらずデータが盗まれても復号できない
サービス提供者すらメッセージの中を見ることができない
犯罪に使われた時に証拠として使えないので問題視されている
Anonifyではクライアントとサーバの通信にE2EEを活用している
サーバ側はEnclaveの中でしかメッセージを復号することができない
Data in useの安全性を確保する
TEEのメリットを最大限活かすために重要な技術
最近、メッセンジャーサービスのE2EEまわりで規制強化論やバックドアしこめなどネガティブな話題が多い
AnonifyのE2EEはメッセンジャーサービスの使用用途とは違うためそれらとは一線を画すものという理解
E2EEを実現するためには公開鍵暗号の仕組みが必要
crypto_boxは公開鍵暗号の実装の一つ
crypto_boxとは
楕円曲線Diffie-Hellman(ディフィー・ヘルマン)鍵交換の実装
鍵共有ともいう
公開鍵暗号の1種
Diffie-Hellman鍵交換はあくまで公開鍵を交換して共通鍵を生成するとことまでで、共通鍵を使った暗号化処理は別途必要になる
データをやりとりする、お互いが公開鍵を交換するだけで共通鍵を生成できるという画期的な手法
glassonion1.icon 初めて知った時は考えた人天才やなと感動しました
cipepser.icon 天才やな
NaClとSodiumとcrypto_boxの関係
NaClとはNetworking and Cryptography libraryの略でセキュア通信、暗号化、復号及びデジタル署名のためのライブラリ
NaClの進化版がSodium(libsodium)
硬い表現をするとNaClの代替実装のうちの一つがSodium
libsodiumをベースにいろんな言語のラッパーライブラリが存在する
Rust、Go、Python、JavaScript、PHP、Ruby、Swift、Kotlinなど
NaClの代替実装にはSodium(libsodium)以外にもTweetNaClなんてものもある
crypto_boxはNaClの機能のうちの一つで公開鍵暗号の機能を提供する。
実装系によってはcrypto_boxではなく単にboxと表現しているものもある
NaClには以下の機能が存在する
公開鍵暗号
crypto_box
秘密鍵暗号
secret_box
ストリーム暗号
ストリームAES
Salsa20
NaCl自体にはChaCha20の実装はなさそう
メッセージ認証
ワンタイム認証
スカラー乗算
RustのNaCl系ライブラリ
sodiumoxide
SGX環境ではlibsodiumのコンパイルができないため使えない(以下を参照のこと)
cipepser.icon うっ
nacl-compat
Pure Rustでlibsodumに依存していない
SodiumというよりはNaClの代替実装の一つと捉えた方が良さそう
現時点ではcrypto_boxの機能のみ提供している
no_stdに対応している。SGX環境でもそのまま使用できる
Anonifyはこちらのライブラリを使用している
今回のサンプルでもこちらを使う
crypto_boxにおける共通鍵暗号の処理
crypto_boxは交換した公開鍵と自身のプライベートキーから共通鍵を生成する
共通鍵をつかった暗号化の処理はSalsa20またはChaCha20を使用する
Salsa20
ストリーム暗号
ちなみにAESはブロック暗号
Anonifyではこちらを使用している
ChaCha20
Salsa20の後継
今回のサンプルではこちらを使う
地味だけど重要なランダムバイト列の生成
crypto_boxは長さ32のランダムなバイト列を使ってキーペアを生成する
ランダムバイト列がシークレットキーになる
暗号処理をするときに使うノンスの生成にもランダムが必要
ランダムバイト列生成には必ず暗号論的擬似乱数生成器または真の乱数生成器を使用すること
暗号論的擬似乱数生成器=CSPRNG(ryptographically secure pseudo random number generator)
CSPRNGではない、単なる擬似乱数生成器のことをPRNG(pseudo random number generator)という
乱数の性質には無作為性、予測不可能性、再現不可能性がある
CSPRNGは無作為性、予測不可能性を満たす。ソフトウェアで実装が可能
真の乱数生成器は無作為性、予測不可能性、再現不可能性の3つ全てを満たす。ソフトウェアだけでは真の乱数を生成することはできない必ずハードウェアが必要(CPUの乱数生成命令を使う)
table: 乱数生成器の種類
無作為性 予測不可能性 再現不可能性
PRNG ○ × ×
CSPRNG ○ ○ ×
真の乱数生成器 ○ ○ ○
真の乱数生成器としてはgetrandomがある
rand_core::OsRngはgetrandomのラッパーライブラリ
システムコールを使用するためno_stdでは使えない
CSPRNGを満たす実装としてはthread_rngやStdRngがある
ハードウェア機能の使用しないのでgetrandomよりも処理速度が速い
その他にも暗号技術をベースにしたCSPRNG実装ライブラリを使う
rand_chachaなど
Seed(種)生成にはgetrandomなどCSPRNG以上を満たす乱数生成器を使うこと
RustのPRNGとCSPRNGのライブラリについてはこちらの表を参照のこと
SGX環境でCSPRNGまたは真の乱数生成器を満たすランダムバイト列を生成する方法
sgx_trts::trts::rsgx_read_randを使う
SGX版のgetrandom的なライブラリ
glassonion1.icon sgx_randじゃなくてsgx_trtsに定義されているので注意。cipepser.iconさんに教えてもらうまで気づかなかった。ちなみにtrts=tRTS(trusted runtime system)
SGX_MODE=SWの時は擬似乱数、SGX_MODE=HWのときは真の乱数を生成してくれる
The rsgx_read_rand function is provided to replace the standard pseudo-random sequence generation functions
inside the enclave, since these standard functions are not supported in the enclave, such as rand, srand, etc.
For HW mode, the function generates a real-random sequence; while in simulation mode, the function generates
a pseudo-random sequence.
mesalock-linux版のgetrandomを使う
sgx_rand::SgxRngを使う
ただしrand_coreのRngCore + CryptoRngトレイトを実装していないのでcrypto_boxと組み合わせて使えない
rand_chachaを使う
sgx_trts::trts::rsgx_read_randまたはmesalock-linux版getrandomで生成したバイト列をシードとして使うこと
rand_coreのRngCore + CryptoRngトレイトを実装する独自乱数生成器を開発する
ランダムバイト列生成部分にsgx_trts::trts::rsgx_read_randを使うと真の乱数生成器を作ることができる
crypto_boxを使ったプログラムをかいてみる
簡易版と発展版を用意
簡易版でcrypto_box自体の使い方を掴んでから、発展版でEnclaveの外と中でデータのやりとり含めて使い方を説明する
サンプルプログラムの構成
簡易版発展版ともに同じ
code: crypto_box
crypto_box/ # サンプルプログラムのディレクトリ
├ Makefile
├ app/
│ ├ Cargo.toml
│ ├ build.rs
│ └ src/
│ └ main.rs
├ enclave/
│ ├ Cargo.toml
│ ├ Enclave.config.xml
│ ├ Enclave.edl # EDL定義ファイル
│ ├ Enclave.lds
│ ├ Enclave_private.pem
│ └ src/
│ └ lib.rs
└ lib/ # 空でOKコンパイル後のファイルが入る
crypto_boxを使ったサンプルプログラム(簡易版)
AliceとBobがそれぞれランダムバイト列からキーペア(シークレットキーと公開鍵)を生成する
ランダムバイト列の生成はrand_chacha::ChaChaRngを使う
rand_chachaはChaChaアルゴリズムを使用する乱数生成器
シードの生成にsgx_trts::trts::rsgx_read_rand使う、他のシードつかうとCSPRNGを満たさなくなることがあるので気をつけること
生成した公開鍵を交換する
ノンスを生成する。ノンスはAliceとBobで共有する
交換した公開鍵、自身のシークレットキーから共通鍵を生成する
メッセージにノンスを添えてメッセージを暗号化する
プログラムにすると以下のようになる(Enclaveの中でcrypto_boxを使う例)
code: lib.rs
use crypto_box::{ChaChaBox, SecretKey};
use rand_chacha::rand_core::SeedableRng;
use sgx_trts::trts::rsgx_read_rand;
use sgx_types::sgx_status_t;
const KEY_SIZE: usize = 32;
pub extern "C" fn ecall_encrypt() -> sgx_status_t {
// ランダムバイト列の生成
rsgx_read_rand(&mut seed).unwrap();
let mut rng = rand_chacha::ChaChaRng::from_seed(seed);
// Aliceのキーペアを生成する
let alice_secret_key = SecretKey::generate(&mut rng);
let alice_public_key = alice_secret_key.public_key();
// Bobのキーペアを生成する
let bob_secret_key = SecretKey::generate(&mut rng);
let bob_public_key = bob_secret_key.public_key();
// ノンスを生成する
let nonce = crypto_box::generate_nonce(&mut rng);
// Aliceがメッセージを暗号化する
let plaintext = "hello Bob";
let ciphertext = ChaChaBox::new(&bob_public_key, &alice_secret_key)
.encrypt(
&nonce,
Payload {
msg: plaintext.as_bytes(),
aad: b"".as_ref(), // Additional Authentication data
},
)
.unwrap();
// 暗号化されたバイト列を出力する
println!("{:?}", ciphertext);
sgx_status_t::SGX_SUCCESS
}
crypto_boxを使ったサンプルプログラム(発展版)
App(Enclaveの外側)でメッセージを暗号化してEnclaveでメッセージを復号するプログラム
AppがAliceでEnclaveがBob
Aliceがメッセージ送信者でBobがメッセージ受信者
crypto_box以外にデータをメモリ上に保存するためにOnceCellを使用している
OnceCellはno_stdで使えないのでmesaloock-linux版を使用している
プログラムの全体像
https://gyazo.com/8d61c7d5600b82bc49e95871425cb181
https://gyazo.com/6c198da011f5dcc26206b6763880d3cf
ダブルクォーテーションで囲まれたメッセージは関数名ではなく関数内の処理を明示的に表すために便宜的に追加したもの
EDLの定義
キーペアを生成して公開鍵を返すECall関数と暗号メッセージを復号するECall関数の二つ
code: Enclave.edl
enclave
{
from "sgx_tstd.edl" import *;
from "sgx_stdio.edl" import *;
from "sgx_backtrace.edl" import *;
from "sgx_tstdc.edl" import *;
trusted
{
public sgx_status_t ecall_get_encryption_key(
size_t out_pubkey_len /* プログラムからは使用しないが宣言しないとSEGVする */
);
public sgx_status_t ecall_decrypt(
size_t in_nonce_len,
size_t in_pubkey_len,
size_t in_ciphertext_len
);
};
untrusted
{
};
};
Appのプログラム
Enclave側の公開鍵を取得する
Enclaveから取得した公開鍵バイナリからPublicKeyオブジェクトを生成する
ランダムバイト列からキーペアを生成する
ランダムバイト列の生成にはrand_core::OsRngを使用する
メッセージを暗号化する
共通鍵の生成
メッセージにノンスを添えて暗号化する
暗号メッセージをEnclave側に送る
code: main.rs
use crypto_box::{
aead::{Aead, Payload},
ChaChaBox, PublicKey, SecretKey,
};
use sgx_types::*;
use sgx_urts::SgxEnclave;
use std::io::Read;
static ENCLAVE_FILE: &'static str = "enclave.signed.so";
const KEY_SIZE: usize = 32;
extern "C" {
fn ecall_get_encryption_key(
eid: sgx_enclave_id_t,
retval: *mut sgx_status_t,
out_pubkey: *mut u8,
out_pubkey_len: usize,
) -> sgx_status_t;
fn ecall_decrypt(
eid: sgx_enclave_id_t,
retval: *mut sgx_status_t,
in_nonce: *const u8,
in_noce_len: usize,
in_pubkey: *const u8,
in_pubkey_len: usize,
in_ciphertext: *const u8,
in_ciphertext_len: usize,
) -> sgx_status_t;
}
fn init_enclave() -> SgxResult<SgxEnclave> {
... 省略 ...
}
fn main() {
let enclave = match init_enclave() {
... エラー処理省略 ...
};
// Enclave側の公開鍵を取得する
let mut retval = sgx_status_t::SGX_SUCCESS;
let key_ptr = key.as_mut_ptr();
let result = unsafe {
ecall_get_encryption_key(enclave.geteid(), &mut retval, key_ptr, key.len())
};
... エラー処理省略 ...
// Enclaveから取得した公開鍵バイナリからPublicKeyオブジェクトを生成する
(&key..).read_exact(&mut buf).unwrap(); let bob_public_key = PublicKey::from(buf);
// ランダムバイト列からキーペアを生成する
let mut rng = rand_core::OsRng;
let alice_secret_key = SecretKey::generate(&mut rng);
let alice_public_key = alice_secret_key.public_key();
println!("Alice's secret key: {:?}", alice_secret_key);
println!("Alice's public key: {:?}", alice_public_key);
let nonce = crypto_box::generate_nonce(&mut rng);
let msg = String::from("hello bob!");
// メッセージを暗号化する
let ciphertext = ChaChaBox::new(&bob_public_key, &alice_secret_key) // 共通鍵の生成
.encrypt( // メッセージにノンスを添えて暗号化する
&nonce,
Payload {
msg: msg.as_bytes(),
aad: b"".as_ref(), // Additional Authentication data },
)
.unwrap();
println!("ciphertext: {:?}", ciphertext);
// 暗号メッセージをEnclave側に送る
let mut retval = sgx_status_t::SGX_SUCCESS;
let b_pubkey = alice_public_key.as_bytes();
let result = unsafe {
ecall_decrypt(
enclave.geteid(),
&mut retval,
nonce.as_ptr(),
nonce.len(),
b_pubkey.as_ptr(),
b_pubkey.len(),
ciphertext.as_ptr(),
ciphertext.len(),
)
};
... エラー処理省略 ...
println!("+ nacl success..."); enclave.destroy();
}
Enclaveのプログラム
公開鍵の取得関数の処理
ランダムバイト列からキーペアを生成する
ランダムバイト列の生成にはsgx_trts::trts::rsgx_read_randを使用する
シークレットキーをメモリに保存する
公開鍵バイナリデータをout_pubkeyに書き込む
code: lib.rs
extern crate sgx_tstd;
use crypto_box::{
aead::{generic_array::GenericArray, Aead, Payload},
ChaChaBox, PublicKey, SecretKey,
};
use once_cell::sync::OnceCell;
use sgx_trts::trts::rsgx_read_rand;
use sgx_tstd::{io::Read, ptr, slice};
use sgx_types::sgx_status_t;
const KEY_SIZE: usize = 32;
static SECRET_KEY: OnceCell<u8; KEY_SIZE> = OnceCell::new(); pub extern "C" fn ecall_get_encryption_key(
out_pubkey: *mut u8,
_out_pubkey_len: usize,
) -> sgx_status_t {
// ランダムバイト列からキーペアを生成する
match rsgx_read_rand(&mut rnd) {
Ok(_) => (),
Err(e) => return e,
};
let secret_key = SecretKey::from(rnd);
let public_key = secret_key.public_key();
println!("Bob's secret key: {:?}", secret_key);
println!("Bob's public key: {:?}", public_key);
// シークレットキーをメモリに保存する
SECRET_KEY.set(secret_key.to_bytes()).unwrap();
// 公開鍵バイナリデータをout_pubkeyに書き込む
let v_pubkey = public_key.as_bytes();
unsafe {
ptr::copy_nonoverlapping(v_pubkey.as_ptr(), out_pubkey, v_pubkey.len());
}
sgx_status_t::SGX_SUCCESS
}
暗号メッセージ復号関数の処理
Appから渡ってきたノンスのバイナリデータからNonce(GenericArray)オブジェクトを生成する
Appから渡ってきた公開鍵バイナリデータからPublicKeyオブジェクトを生成する
暗号メッセージをスライスに変換する
メモリに保存されたシークレットキーバイナリデータからSecretKeyオブジェクトを生成する
暗号メッセージを復号する
共通鍵の生成
暗号メッセージにノンスを添えて復号する
複合したメッセージを確認する
code:lib.rs
pub extern "C" fn ecall_decrypt(
in_nonce: *const u8,
in_nonce_len: usize,
in_pubkey: *const u8,
in_pubkey_len: usize,
in_ciphertext: *mut u8,
in_ciphertext_len: usize,
) -> sgx_status_t {
// Appから渡ってきたノンスのバイナリデータからNonce(GenericArray)オブジェクトを生成する
let nonce = unsafe { slice::from_raw_parts(in_nonce, in_nonce_len) };
let nonce = GenericArray::from_slice(nonce);
// Appから渡ってきた公開鍵バイナリデータからPublicKeyオブジェクトを生成する
let mut pubkey_slice = unsafe { slice::from_raw_parts(in_pubkey, in_pubkey_len) };
pubkey_slice.read_exact(&mut buf).unwrap();
let alice_public_key = PublicKey::from(buf);
// 暗号メッセージをスライスに変換する
let ciphertext = unsafe { slice::from_raw_parts(in_ciphertext, in_ciphertext_len) };
// メモリに保存されたシークレットキーバイナリデータからSecretKeyオブジェクトを生成する
let b = SECRET_KEY.get().unwrap();
let bob_secret_key = SecretKey::from(*b);
// 暗号メッセージを復号する
let decrypted = ChaChaBox::new(&alice_public_key, &bob_secret_key) // 共通鍵の生成
.decrypt( // 暗号メッセージにノンスを添えて復号する
&nonce,
Payload {
msg: ciphertext,
aad: b"".as_ref(),
},
)
.unwrap();
// 複合したメッセージを確認する
let decrypted = sgx_tstd::str::from_utf8(&decrypted).unwrap();
println!("decrypted message: {}", decrypted);
sgx_status_t::SGX_SUCCESS
}
実行結果
実行結果は以下の通り
code: bash
+ Init Enclave Successful 226495100354562! Bob's secret key: SecretKey(...)
Bob's public key: PublicKey(144, 170, 18, 206, 218, 16, 97, 150, 229, 251, 229, 250, 237, 31, 225, 131, 119, 131, 66, 229, 234, 175, 146, 130, 19, 219, 107, 88, 250, 188, 162, 7) Alice's secret key: SecretKey(...)
Alice's public key: PublicKey(1, 246, 204, 11, 67, 188, 111, 245, 209, 210, 129, 98, 157, 74, 248, 242, 209, 76, 138, 238, 125, 15, 1, 241, 29, 205, 47, 159, 209, 123, 16, 22) encrypted message: 46, 231, 123, 88, 36, 26, 42, 106, 9, 172, 102, 100, 170, 84, 93, 139, 222, 29, 185, 214, 24, 112, 73, 60, 37, 172 decrypted message: hello bob!
注意事項
実運用ではランダム生成したバイト列をうっかりログに吐いたりしないこと
ノンスは使いまわさずメッセージのやりとりごとに必ず再生成すること
おまけ
この記事で紹介したコード以外に以下2つのサンプルを作成したので興味があればどうぞ。
Enclaveの中で暗号化するサンプル
rand_coreのRngCore + CryptoRngトレイトを実装する独自ランダム生成器のサンプル
まとめ
AnonifyではE2EEを実現するための暗号技術としてcrypto_boxを使っている
crypto_boxは公開鍵暗号の一種
nacl-compatのcrypto_boxはSGX環境でも普通に使うことができる
ただしランダムバイト列の生成にはひと工夫必要
結局のところランダムバイト列生成が一番大事
暗号処理とは全然関係ない部分で、ECall関数を介したデータのやりとりがすごく大変だった
ptr::copy_nonoverlappingを使ったデータのコピーやslice::from_raw_partsからslice生成は慣れないとつらい
暗号系ライブラリは固定長配列を使いがちなんで固定長データの取り扱いにも慣れた方が良い
read_exactあたり
次回は生成したシークレットキーを安全に保管するためのシーリングについて解説する
LayerX Labsの暗号技術入門 第3版の輪読会に参加して一通り暗号技術について勉強したこともあり、そこまで新たな発見はないかなと油断していました。ところが実際に実装をはじめてみて、Rustのいろんなライブラリのソースコードを読んだりライブラリを使ったりしてみると新たな発見がたくさんありました。あらためて実践が大事だと思いました。私から一つ言えることはランダムバイト列の生成めっちゃ大事、絶対に手を抜いたらだめということです。(文責・藤田) Anonify解体新書 | 連載一覧(全8回)